﻿
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows;

namespace Mandarine.MVVM.ViewModel
{
    /// <summary>
    ///  MVVMパターンにおけるViewModelの実装のベースクラスを提供します
    /// 
    ///  <pre>単体テストを容易にするためにも、ViewModelクラスにViewへの依存性を
    ///  持たせないでください。</pre>
    /// </summary>
    public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        private readonly IDictionary<string, IList<string>> _errors = 
            new Dictionary<string, IList<string>>(); 
        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        /// <summary>
        /// デザインモードか否かを判定します
        /// </summary>
        public bool IsInDesignMode
        {
            get { return DesignerProperties.IsInDesignTool; }
        }
        /// <summary>
        /// 全てのプロパティを検査(バリデート)します
        /// </summary>
        /// <returns>バリデートOKの場合trueが戻ります</returns>
        public bool ValidateAll()
        {
            _errors.Clear();

            Type objectType = this.GetType();
            PropertyInfo[] properties = objectType.GetProperties();
            foreach (PropertyInfo property in properties)
            {
                if (property.GetCustomAttributes(typeof(ValidationAttribute)
                    , true).Any())
                {
                    object value = property.GetValue(this, null);
                    ValidateProperty(property.Name, value);
                }
            }

            return !HasErrors;
        }
        /// <summary>
        /// 全てのプロパティを検査(バリデート)します
        /// </summary>
        /// <returns>バリデートOKの場合trueが戻ります</returns>
        public static bool ValidateAll(ViewModel vm)
        {
            var objectToValidate = vm;
            vm._errors.Clear();

            Type objectType = objectToValidate.GetType();
            PropertyInfo[] properties = objectType.GetProperties();
            foreach (PropertyInfo property in properties)
            {
                if (property.GetCustomAttributes(typeof(ValidationAttribute)
                    , true).Any())
                {
                    object value = property.GetValue(objectToValidate, null);
                    vm.ValidateProperty(property.Name, value);
                }
            }

            return !vm.HasErrors;
        }

        /// <summary>
        /// プロパティを検査(バリデート)します
        /// </summary>
        /// <param name="propertyExpresssion"></param>
        /// <param name="value"></param>
        protected void ValidateProperty<T>(Expression<Func<T>> propertyExpresssion, object value)
        {
            var propertyName = ViewModel.ExtractPropertyName(propertyExpresssion);
            ValidateProperty(propertyName, value);
        }
        /// <summary>
        /// プロパティを検査(バリデート)します
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="value"></param>
        protected void ValidateProperty(string propertyName, object value)
        {
            var results = new List<ValidationResult>();
            bool isValid = Validator.TryValidateProperty(
                value
                , new ValidationContext(this, null, null)
                {
                    MemberName = propertyName
                }
                , results);

            if (isValid)
            {
                RemoveErrorsForProperty(propertyName);
            }
            else
            {
                AddErrorsForProperty(propertyName, results);
            }
            NotifyErrorsChanged(propertyName);
        }
        /// <summary>
        /// バリデートの結果を追加します
        /// </summary>
        /// <param name="propertyName"></param>
        /// <param name="validationResults"></param>
        private void AddErrorsForProperty(string propertyName, IEnumerable<ValidationResult> validationResults)
        {
            RemoveErrorsForProperty(propertyName);
            _errors.Add(propertyName
                , validationResults.Select(vr => vr.ErrorMessage).ToList());
        }
        /// <summary>
        /// バリデートの結果を除去します
        /// </summary>
        /// <param name="propertyName"></param>
        private void RemoveErrorsForProperty(string propertyName)
        {
            if (_errors.ContainsKey(propertyName))
                _errors.Remove(propertyName);
        }
        /// <summary>
        /// プロパティ変更時のイベントを通知します
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="propertyExpresssion"></param>
        protected void NotifyPropertyChanged<T>(Expression<Func<T>> propertyExpresssion)
        {
            var propertyName = ViewModel.ExtractPropertyName(propertyExpresssion);
            NotifyPropertyChanged(propertyName);
        }
        /// <summary>
        ///  プロパティ変更時のイベントを通知します
        /// </summary>
        /// <param name="propertyName"></param>
        protected virtual void NotifyPropertyChanged(string propertyName)
        {
            //プロパティ変更時イベントの処理
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        /// <summary>
        /// エラー変更変更時のイベントを通知します
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="propertyExpresssion"></param>
        protected void NotifyErrorsChanged<T>(Expression<Func<T>> propertyExpresssion)
        {
            var propertyName = ViewModel.ExtractPropertyName(propertyExpresssion);
            NotifyErrorsChanged(propertyName);
        }
        /// <summary>
        /// エラー変更時のイベントを通知します
        /// </summary>
        /// <param name="propertyName"></param>
        protected void NotifyErrorsChanged(string propertyName)
        {
            //エラー変更時イベントの処理
            if (ErrorsChanged != null)
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));

            NotifyPropertyChanged(()=> Errors);
        }
        /// <summary>
        /// UIスレッドに同期してアクションを実行します
        /// </summary>
        /// <param name="action"></param>
        protected void ExecuteOnUIThread(Action action)
        {
            var dispatcher = Deployment.Current.Dispatcher;

            if (dispatcher.CheckAccess())
            {
                action();
            }
            else
            {
                dispatcher.BeginInvoke(action);
            }
        }
        /// <summary>
        /// 式からプロパティ名を抽出します
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="propertyExpression"></param>
        /// <returns></returns>
        public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
        {
            if (propertyExpression == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }

            var memberExpression = propertyExpression.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new ArgumentException("propertyExpression");
            }

            var property = memberExpression.Member as PropertyInfo;
            if (property == null)
            {
                throw new ArgumentException("propertyExpression");
            }

            var getMethod = property.GetGetMethod(true);
            if (getMethod.IsStatic)
            {
                throw new ArgumentException("propertyExpression");
            }

            return memberExpression.Member.Name;
        }

        #region INotifyDataErrorInfo メンバ

        public IEnumerable GetErrors(string propertyName)
        {
            if (_errors.ContainsKey(propertyName))
            {
                IList<string> propertyErrors = _errors[propertyName];
                foreach (var propertyError in propertyErrors)
                {
                    yield return propertyError;
                }
            }
            yield break;
        }

        public bool HasErrors
        {
            get {
                return _errors.Count > 0;
            }
        }

        #endregion

        /// <summary>
        /// インデクサ (エラー)
        /// </summary>
        /// <param name="columnName"></param>
        /// <returns></returns>
        public IEnumerable<string> this[string columnName]
        {
            get {
                return _errors[columnName];
            }
        }
        /// <summary>
        /// 全てのエラー
        /// </summary>
        public IEnumerable<string> Errors
        {
            get {
                return _errors.Values.SelectMany(error => error);
            }
        }

    }
}